package de.mmis.utilities.genericPublisher;

import java.awt.Dialog.ModalityType;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeoutException;

import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.xml.ws.Holder;

import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;

import de.mmis.core.base.ObjectDescriptor;
import de.mmis.core.base.Quittable;
import de.mmis.core.base.Type;
import de.mmis.core.base.abstracttree.LeafNode;
import de.mmis.core.base.abstracttree.Tree;
import de.mmis.core.base.abstracttree.serializer.DeserializableException;
import de.mmis.core.base.abstracttree.serializer.SerializerHelper;
import de.mmis.core.base.abstracttree.serializer.SerializerHelper.MethodInvocationException;
import de.mmis.core.base.abstracttree.serializer.SerializerHelper.NoMatchingMethodFoundException;
import de.mmis.core.base.event.Event;
import de.mmis.core.base.event.Observable;
import de.mmis.core.base.event.Observer;
import de.mmis.core.base.gclass.GClass;
import de.mmis.core.base.informationprovider.DeviceInformationProvider;
import de.mmis.core.base.infrastructure.Infrastructure;
import de.mmis.core.base.infrastructure.InfrastructureEvent;
import de.mmis.core.base.infrastructure.InfrastructureEvent.InfrastructureEventType;
import de.mmis.core.base.infrastructure.InfrastructureException;
import de.mmis.core.base.infrastructure.PublishedDeviceDescription;
import de.mmis.core.base.infrastructure.PublishedDeviceDescription.PublishType;
import de.mmis.core.infrastructure.DistributedInfrastructure;
import de.mmis.core.pojop.NetworkProxy;
import de.mmis.core.pojop.NetworkPublisher;
import de.mmis.core.pojop.NetworkPublisher.ServerType;
import de.mmis.core.pojop.NetworkPublisherInformationProvider;
import de.mmis.core.pojop.server.Server;
import de.mmis.core.publishsubscribe.EventHelper;
import de.mmis.core.publishsubscribe.PlainEvent;
import de.mmis.core.publishsubscribe.PublishSubscribe;
import de.mmis.core.publishsubscribe.PublishSubscribe.PublishSubscribeException;
import de.mmis.core.publishsubscribe.PublishSubscribeImpl;
import de.mmis.core.publishsubscribe.filter.Filter;
import de.mmis.core.tuplespace.Tuplespace;
import de.mmis.core.tuplespace.TuplespaceException;
import de.mmis.core.tuplespace.TuplespaceMultikeyMap;
import de.mmis.core.tuplespace.TuplespaceProxy;
import de.mmis.core.tuplespace.TuplespacePublisher;
import de.mmis.core.tuplespace.TuplespacePublisherInformationProvider;
import de.mmis.utilities.consoleWrapper.ConsoleWrapper;

/**
 * <p>
 * The publishing methods take a serialized Object as an argument and publish it
 * using the Network- or TuplespacePublisher. Additionally adds the published
 * devices automatically in the infrastructure.
 * 
 * <ul>
 * <li>{@link #publishPOJOP(Object, String, int, ServerType, boolean, boolean)}</li>
 * <li>{@link #publishTuplespace(Object, String, String, int, boolean)}</li>
 * <li>{@link #publishPubSub(Observable, String, boolean)}</li>
 * </ul>
 * </p>
 * <p>
 * The GenericPublisher can also write additional information of the device
 * (such as the position) to a {@link Tuplespace}. These information are written
 * using the {@link #writeInformation(String, String, Object) writeInformation}
 * method.
 * </p>
 * <p>
 * This class can either be used by another class, but the
 * {@link GenericPublisherMain} class contains a main method taking options from
 * the command line.
 * </p>
 * 
 * 
 * @author Martin Nyolt
 * 
 */
public class GenericPublisher {

	private final static Logger LOGGER;

	static {
		LOGGER = Logger.getLogger(GenericPublisher.class);
		Logger root = Logger.getRootLogger();

		// check if log4j has been configured, i.e. the root logger has at least
		// one appender
		// if not, add a simple console appender to the root logger

		boolean inited = root.getAllAppenders().hasMoreElements();

		if (!inited) {
			// must use default constructor, to not call activateOptions twice
			ConsoleAppender app = new ConsoleAppender();
			app.setTarget(ConsoleAppender.SYSTEM_ERR);
			app.setLayout(new PatternLayout("%-5p [%t]: %m%n"));
			app.setFollow(true);
			app.activateOptions();

			root.addAppender(app);
			root.setLevel(Level.ERROR);
		}
	}

	public static class AddDeviceInfo {
		private Object object;
		private Class<?> clazz;
		private Tree methodCall;
		private PublishType type;
		private PublishedDeviceDescription pattern;

		/** The id of the object to add */
		private String id;

		/** if the notification is for new or removed devices */
		private boolean add;

		/**
		 * contains all IDs of all Devices that were added due to a
		 * Infrastructure ANSWER event, to prevent it from being added multiple
		 * times (but call it every time for NEW DEVICE)
		 */
		private HashSet<String> answerDevices = new HashSet<String>();

		public AddDeviceInfo(Object object, Class<?> clazz, String id,
				Tree methodCall, PublishType type, boolean add) {
			this.object = object;
			this.clazz = clazz;
			this.methodCall = methodCall;
			this.type = type;
			this.id = id;
			pattern = new PublishedDeviceDescription(
					id.equals("*") ? null : id, null, type, null, -1);
			this.add = add;
		}
	}

	private ConsoleWrapper consoleWrapper;

	/** The property-Map of the POJOP */
	HashMap<String, Map<String, Object>> properties = new HashMap<String, Map<String, Object>>();

	HashMap<String, Object> idmap = new HashMap<String, Object>();

	private LinkedList<PublishHandle> handles = new LinkedList<PublishHandle>();
	private LinkedList<Integer> pubSubSubscribeIDs = new LinkedList<Integer>();
	private LinkedList<AddDeviceInfo> addDeviceInfos = new LinkedList<AddDeviceInfo>();

	private String tuplespaceHost = null;
	private int tuplespacePort = 0;
	private Tuplespace space = null;
	private DistributedInfrastructure infrastructure;
	private InetAddress bindAddress = null;
	private String localhost;
	{
		try {
			localhost = InetAddress.getLocalHost().getHostName();
		} catch (UnknownHostException e) {
			localhost = "unknown";
		}
	}

	private Integer commandHistoryLeaseTime = null;

	/**
	 * sets and enables the command History of the {@link TuplespacePublisher}.
	 * 
	 * @param commandHistoryLeaseTime
	 *            Lease time for the commands in milliseconds
	 */
	public void setCommandHistoryLeaseTime(int commandHistoryLeaseTime) {
		this.commandHistoryLeaseTime = new Integer(commandHistoryLeaseTime);

	}

	/**
	 * sets the preferred bindAddress of the {@link Infrastructure}. Must be
	 * called before any publish method.
	 * 
	 * @param bindAddress
	 */
	public void setBindAddress(InetAddress bindAddress) {
		this.bindAddress = bindAddress;
	}

	private final static Object setupInfrastructureMonitor = new Object();

	private void setupInfrastructure() throws InfrastructureException {
		synchronized (setupInfrastructureMonitor) {
			if (infrastructure != null)
				return;

			infrastructure = DistributedInfrastructure
					.createInstance(bindAddress);
			// get (updated) bindAddress
		}

		System.out.println("CW: " + consoleWrapper);

		if (consoleWrapper == null)
			return;

		JMenuBar menu = consoleWrapper.getMenu();
		JMenu menu_inf = new JMenu("Infrastructure");

		JMenuItem rebind = new JMenuItem("rebind");
		menu.add(menu_inf);
		menu_inf.add(rebind);

		rebind.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent _) {
				SelectBindAddressDialog select = new SelectBindAddressDialog(
						consoleWrapper);
				select.setModalityType(ModalityType.APPLICATION_MODAL);
				select.setVisible(true);
				InetAddress a = select.getSelectedAddress();
				if (a != null)
					try {
						infrastructure.rebind(a);
					} catch (InfrastructureException e) {
						e.printStackTrace();
					}
			}
		});

		consoleWrapper.validate();
	}

	/**
	 * Blocks until a Tuplespace as found
	 * 
	 * @return {@link PublishedDeviceDescription}
	 * @throws Exception
	 */
	PublishedDeviceDescription discoverSpace() throws Exception {
		final PublishedDeviceDescription pattern = new PublishedDeviceDescription(
				"Tuplespace", null, PublishType.SOCKET, null, -1);

		final CountDownLatch wait = new CountDownLatch(1);
		final Holder<PublishedDeviceDescription> d = new Holder<PublishedDeviceDescription>();

		try {
			setupInfrastructure();

			Observer<InfrastructureEvent> observer = new Observer<InfrastructureEvent>() {
				@Override
				public void notify(
						Observable<? extends InfrastructureEvent> sender,
						InfrastructureEvent event) {
					if (event.getEventType() != InfrastructureEventType.ANSWER_DEVICE
							&& event.getEventType() != InfrastructureEventType.NEW_DEVICE)
						return;

					PublishedDeviceDescription descr = event.device;
					if (!descr.matches(pattern))
						return;

					wait.countDown();
					d.value = descr;
				}
			};

			infrastructure.addObserver(observer);
			infrastructure.searchDevice(pattern);

			while (wait.getCount() > 0)
				try {
					wait.await();
					infrastructure.removeObserver(observer);
				} catch (InterruptedException e) {
					// do over
				}
		} catch (InfrastructureException e1) {
			throw new Exception(e1);
		}

		return d.value;
	}

	/**
	 * Blocks until a Tuplespace was found
	 * 
	 * @return a proxy
	 * @throws Exception
	 */
	Tuplespace createSpace() throws Exception {
		PublishedDeviceDescription descr = discoverSpace();
		try {
			return NetworkProxy.createProxy(Tuplespace.class, descr.getHost()
					.getHostAddress(), descr.getPort());
		} catch (UnknownHostException e) {
			throw new Exception("Could not connect to Tuplespace", e);
		} catch (IOException e) {
			throw new Exception("Could not connect to Tuplespace", e);
		}
	}

	private PublishedDeviceDescription getDescr(String id, PublishType type,
			String hostIP, int port) {
		return new PublishedDeviceDescription(id, localhost, type, hostIP,
				port, properties.get(id));
	}

	/**
	 * registers the object as an producer by the Publish/Subscribe system.
	 * 
	 * @return a {@link PublishHandle handle} containing some information. The
	 *         handle can also be found in the {@link List} returned by
	 *         {@link #getHandles()}.
	 * 
	 * @throws Exception
	 *             when the object is not an {@link Observable} or when
	 *             something went wrong.
	 */
	public PubSubPublishHandle publishPubSub(Observable<?> prod, String id,
			boolean useInfrastructure) throws Exception {
		if (prod == null)
			throw new Exception("object to publish was not set.");

		PublishSubscribe pubsub;
		try {
			pubsub = PublishSubscribeImpl.getInstance();
		} catch (PublishSubscribeException e) {
			throw new Exception(e);
		}

		try {
			pubsub.addObservable(prod, id);
		} catch (PublishSubscribeException e) {
			throw new Exception(e);
		}

		PublishedDeviceDescription descr = null;

		if (useInfrastructure) {
			try {
				setupInfrastructure();
			} catch (InfrastructureException e) {
				throw new Exception(e);
			}

			try {
				descr = getDescr(id, PublishType.PUBSUB, InetAddress
						.getLocalHost().getHostAddress(), 1);
			} catch (UnknownHostException e) {
				throw new Exception(e);
			}
		}

		if (useInfrastructure)
			try {
				infrastructure.addDevice(descr);
			} catch (InfrastructureException e) {
				throw new Exception(e);
			}

		PubSubPublishHandle handle = new PubSubPublishHandle(id, descr, prod);

		handles.add(handle);
		return handle;
	}

	/**
	 * Publishes the Object on a {@link Tuplespace} and adds the information to
	 * the {@link Infrastructure}.
	 * 
	 * @param address
	 *            The {@link InetAddress} of the Tuplespace
	 * @param port
	 *            The corresponding port
	 * 
	 * 
	 * @return a {@link PublishHandle handle} containing some information. The
	 *         handle can also be found in the {@link List} returned by
	 *         {@link #getHandles()}.
	 * 
	 * @throws Exception
	 *             when something went wrong. See {@link Exception#getCause()}
	 *             for mor detail.
	 */
	public TuplespacePublishHandle publishTuplespace(Object object, String id,
			String address, int port, boolean useInfrastructure)
			throws Exception {
		if (object == null)
			throw new Exception("object to publish was not set.");

		TuplespacePublisher publisher;

		if (address == null || port <= 0)
			return publishTuplespace(object, id, useInfrastructure);

		try {
			publisher = new TuplespacePublisher(object, id, address, port);
			if (commandHistoryLeaseTime != null)
				publisher.setCommandHistoryLease(commandHistoryLeaseTime
						.intValue());
		} catch (TuplespaceException e) {
			throw new Exception(e);
		}

		PublishedDeviceDescription descr = getDescr(id, PublishType.TUPLESPACE,
				address, port);
		TuplespacePublishHandle handle = new TuplespacePublishHandle(id, descr,
				address, port, publisher);

		if (useInfrastructure)
			try {
				setupInfrastructure();
				infrastructure.addDevice(descr);
			} catch (InfrastructureException e) {
				publisher.close();
				throw new Exception(e);
			}

		handles.add(handle);
		return handle;
	}

	/**
	 * Publishes the Object on a {@link Tuplespace} set by
	 * {@link #setTuplespace(InetAddress, int) setTuplespace} and adds the
	 * information to the {@link Infrastructure}.
	 * 
	 * 
	 * @return a {@link PublishHandle handle} containing some information. The
	 *         handle can also be found in the {@link List} returned by
	 *         {@link #getHandles()}.
	 * 
	 * @throws Exception
	 *             when something went wrong. See {@link Exception#getCause()}
	 *             for mor detail.
	 */
	public TuplespacePublishHandle publishTuplespace(Object object, String id,
			boolean useInfrastructure) throws Exception {
		setDefaultTuplespace();

		return publishTuplespace(object, id, tuplespaceHost, tuplespacePort,
				useInfrastructure);
	}

	private static PublishType serverTypeToPublishType(ServerType type) {
		switch (type) {
		case HTTP:
			return PublishType.HTTP;
		case Socket:
			return PublishType.SOCKET;
		case Telnet:
			return PublishType.TELNET;
		}

		return null;
	}

	/**
	 * Publishes the Object using POJOP and adds the information to the
	 * {@link Infrastructure}.
	 * 
	 * @param port
	 *            The port to listen. Use 0 for a random port. Use
	 *            {@link POJOPPublishHandle#getPort()} to get the choosen port.
	 * 
	 * @param type
	 *            the {@link ServerType} to use.
	 * 
	 * @return a {@link PublishHandle handle} containing some information. The
	 *         handle can also be found in the {@link List} returned by
	 *         {@link #getHandles()}.
	 * 
	 * @throws Exception
	 *             when something went wrong. See {@link Exception#getCause()}
	 *             for mor detail.
	 */
	public POJOPPublishHandle publishPOJOP(Object object, String id, int port,
			ServerType type, boolean useInfrastructure, boolean implicit)
			throws Exception {
		if (object == null)
			throw new Exception("object to publish was not set.");

		Server server;

		try {
			NetworkPublisher networkPublisher = new NetworkPublisher(object,
					id, properties.get(id));
			server = networkPublisher.publish(type, port, !implicit);
		} catch (IOException e) {
			throw new Exception(e);
		}

		port = server.getPort();
		PublishedDeviceDescription descr = null;
		if (useInfrastructure) {
			try {
				setupInfrastructure();
			} catch (InfrastructureException e) {
				throw new Exception(e);
			}
			descr = getDescr(id, serverTypeToPublishType(type), infrastructure
					.getBindAddress().getHostAddress(), port);
		}

		POJOPPublishHandle handle = new POJOPPublishHandle(id, descr, type,
				port, server);

		if (useInfrastructure)
			try {
				infrastructure.addDevice(descr);
			} catch (InfrastructureException e) {
				server.stopListening();
				throw new Exception(e);
			}

		handles.add(handle);
		return handle;
	}

	/**
	 * writes some additional information to the tuplespace
	 * 
	 */
	public void writeInformation(final String id, final String key,
			final Object value) {
		Map<String, Object> property = properties.get(id);
		if (property == null) {
			property = new HashMap<String, Object>();
			properties.put(id, property);
		}
		property.put(key, value);

		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					setDefaultTuplespace();

					// remove synchronization when map can be initialized
					// safely
					synchronized (space) {
						try {
							TuplespaceMultikeyMap map = new TuplespaceMultikeyMap(
									space, "informationprovider", new LeafNode(
											"device-information"));

							if (!map.isPresent())
								map.initMap();

							map.setValue(Tree.fromObject(value),
									Tree.fromObject(id), Tree.fromObject(key));

						} catch (java.lang.Exception e) {
							e.printStackTrace();
						}
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}, "write information " + id + " " + key + " " + value).start();

	}

	/**
	 * Register for events in the Publish/Subscribe system of the given class
	 * that match the filter and notify the object.The object must be
	 * Observable<? super CLASS>.
	 * 
	 * @param clazz
	 *            The class of the Events
	 * @param filter
	 *            The filter to match
	 * 
	 * @throws Exception
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public void addSubscribeObserver(final Object object,
			Class<? extends Event> clazz, Filter filter) throws Exception {
		if (object == null)
			throw new NullPointerException("object must not be null");

		PublishSubscribeImpl ps;
		try {
			ps = PublishSubscribeImpl.getInstance();
		} catch (PublishSubscribeException e) {
			throw new Exception("Can't create instance of Publish/Subscribe", e);
		}

		if (!(object instanceof Observer))
			throw new Exception("Object is not observable");

		if (clazz == null) {
			GClass[] t = GClass.getGenericsOf(
					GClass.fromClass(object.getClass()), Observer.class);

			if (t.length != 1)
				throw new Exception(
						"Observer must have exactly one type parameter");

			Class<?> clazz_ = t[0].getBaseClass();
			if (!Event.class.isAssignableFrom(clazz_))
				throw new Exception("Inferred class " + clazz_
						+ " is not an Event");

			clazz = (Class<? extends Event>) clazz_;
		}

		try {
			Integer id = ps.subscribe(clazz, filter, (Observer) object);
			pubSubSubscribeIDs.add(id);
			return;
		} catch (PublishSubscribeException e) {
			throw new Exception("Can't subscribe object as observer", e);
		}
	}

	/**
	 * <p>
	 * Register for events in the Publish/Subscribe system of the given class
	 * that match the filter and notify the object. The method may have at most
	 * one argument taking an Event. In this case, the parameter type is taken
	 * as the event class if clazz is null.
	 * </p>
	 * 
	 * <p>
	 * If the method takes no parameters and clazz is null, {@link Event} will
	 * be used.
	 * </p>
	 * 
	 * @param clazz
	 *            The class of the Events
	 * @param filter
	 *            The filter to match
	 * @param methodName
	 *            The method to call, or <code>null</code>
	 * 
	 * @throws Exception
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public void addSubscribeMethod(final Object object,
			Class<? extends Event> clazz, Filter filter, String methodName)
			throws Exception {
		if (object == null)
			throw new NullPointerException("object must not be null");

		if (methodName == null)
			throw new NullPointerException("methodName must not be null");

		PublishSubscribeImpl ps;
		try {
			ps = PublishSubscribeImpl.getInstance();
		} catch (PublishSubscribeException e) {
			throw new Exception("Can't create instance of Publish/Subscribe", e);
		}

		Method method = null;
		if (clazz != null) {
			try {
				method = object.getClass().getMethod(methodName, clazz);
			} catch (SecurityException e) {
				throw new Exception(e);
			} catch (NoSuchMethodException e) {
				// handle below
			}

			if (method == null)
				try {
					method = object.getClass().getMethod(methodName);
				} catch (SecurityException e) {
					throw new Exception(e);
				} catch (NoSuchMethodException e) {
					throw new Exception(e);
				}
		} else {
			for (Method m : object.getClass().getMethods()) {
				if (!m.getName().equals(methodName))
					continue;

				if (m.getParameterTypes().length > 1)
					continue;

				if (m.getParameterTypes().length == 0) {
					method = m;
					clazz = Event.class;
					// try to find a method with parameter
					continue;
				}

				Class<?> clazz_ = m.getParameterTypes()[0];
				if (!Event.class.isAssignableFrom(clazz_)) {
					if (method == null)
						throw new Exception("Inferred class " + clazz_
								+ " is not an Event");
					continue;
				}

				clazz = (Class<? extends Event>) clazz_;
				method = m;
				continue;
			}

			if (method == null)
				throw new Exception("Could not find any method " + methodName
						+ " with suitable signature");
		}

		final Method m = method;

		try {
			Integer id = ps.subscribe(clazz, filter, new Observer() {
				@Override
				public void notify(Observable sender, Event event) {
					try {
						m.invoke(object, event);
					} catch (IllegalArgumentException e) {
						LOGGER.error("Could not invoke notify method", e);
					} catch (IllegalAccessException e) {
						LOGGER.error("Could not invoke notify method", e);
					} catch (InvocationTargetException e) {
						LOGGER.error("Could not invoke notify method", e);
					}
				}
			});

			pubSubSubscribeIDs.add(id);
			return;
		} catch (PublishSubscribeException e) {
			throw new Exception("Can't subscribe as observer", e);
		}
	}

	/**
	 * <p>
	 * Register for events in the Publish/Subscribe system of the given class
	 * that match the filter and notify the object. If clazz is not given,
	 * {@link Event} is used.
	 * </p>
	 * 
	 * <p>
	 * The method given as an {@link Tree} will be used to notify the object, e.
	 * g. <code>(dimLight 0.5)</code>. You can use @EVENT to pass the event as a
	 * parameter and @EVENT_field for a specific field.
	 * </p>
	 * 
	 * @param clazz
	 *            The class of the Events
	 * @param filter
	 *            The filter to match
	 * @param methodCall
	 *            The method to call as an {@link Tree}.
	 * 
	 * @throws Exception
	 */
	public void addSubscribeMethodCall(final Object object,
			Class<? extends Event> clazz, Filter filter, final Tree methodCall)
			throws Exception {
		if (object == null)
			throw new NullPointerException("object must not be null");

		if (methodCall == null)
			throw new NullPointerException("methodCall must not be null");

		if (clazz == null)
			clazz = Event.class;

		final Class<? extends Event> cl = clazz;

		PublishSubscribeImpl ps;
		try {
			ps = PublishSubscribeImpl.getInstance();
		} catch (PublishSubscribeException e) {
			throw new Exception("Can't create instance of Publish/Subscribe", e);
		}

		try {
			Integer id = ps.subscribe(clazz, filter, null,
					new Observer<PlainEvent>() {
						@Override
						public void notify(
								Observable<? extends PlainEvent> sender,
								PlainEvent event) {
							addSubscribeMethodCallNotify(object, cl,
									methodCall, event);
						}
					});

			pubSubSubscribeIDs.add(id);
			return;
		} catch (PublishSubscribeException e) {
			throw new Exception("Can't subscribe as observer", e);
		}
	}

	private void addSubscribeMethodCallNotify(final Object object,
			Class<? extends Event> clazz, final Tree methodCall,
			final PlainEvent plainEvent) {
		Map<String, Tree> data = plainEvent.getEventData();
		HashMap<String, Object> map;
		synchronized (idmap) {
			map = new HashMap<String, Object>(idmap);
		}

		LinkedList<Entry<String, Tree>> metaEntries = new LinkedList<Map.Entry<String, Tree>>();

		// add PlainEvent data, i.e. Trees, and put it to @PLAINEVENT_x
		// also try to deserialize these objects and put it to @EVENT_x
		// these are later probably replaced by the real event data (see below)
		map.put("@PLAINEVENT", data);

		for (Entry<String, Tree> entry : data.entrySet()) {
			String key = entry.getKey();
			map.put("@PLAINEVENT_" + key, entry.getValue());
			try {
				map.put("@EVENT_" + key, entry.getValue().getAs(Object.class));
			} catch (DeserializableException e) {
				// just ignore
			}

			if (key.startsWith(":"))
				metaEntries.add(entry);
		}

		// add Event data, i.e. the objects itself
		// replace existing @EVENT_x fields

		Event event = EventHelper.plain2event(plainEvent, clazz);
		map.put("@EVENT", event);

		for (Entry<String, Object> entry : EventHelper.getEventData(event)
				.entrySet())
			map.put("@EVENT_" + entry.getKey(), entry.getValue());

		// as the event data does not contain any meta-data, add it seperately:
		for (Entry<String, Tree> entry : metaEntries)
			try {
				map.put("@EVENT_" + entry.getKey(),
						entry.getValue().getAs(Object.class));
			} catch (DeserializableException e) {
				LOGGER.error(
						"Can't deserialize the meta-data field "
								+ entry.getKey(), e);
			}

		ObjectDescriptor descr = new ObjectDescriptor(object, false);
		try {
			SerializerHelper.invokeObjectMethod(descr, methodCall, object
					.getClass().getClassLoader(), map);
		} catch (MethodInvocationException e) {
			LOGGER.error(
					"Could not invoke method " + methodCall + " on object", e);
		} catch (NoMatchingMethodFoundException e) {
			LOGGER.error(
					"Could not invoke method " + methodCall + " on object", e);
		}
	}

	private void addDeviceNotify(InfrastructureEvent event) {
		PublishedDeviceDescription device = event.device;

		if (event.getEventType() != InfrastructureEventType.ANSWER_DEVICE
				&& event.getEventType() != InfrastructureEventType.NEW_DEVICE
				&& event.getEventType() != InfrastructureEventType.DEVICE_REMOVED)
			return;

		if (device.getType() != PublishType.SOCKET
				&& device.getType() != PublishType.TUPLESPACE)
			return;

		LOGGER.trace("addDeviceNotify: got InfrastructureEvent " + event);

		String id = device.getID();
		String host = device.getHost().getHostAddress();
		int port = device.getPort();

		synchronized (addDeviceInfos) {
			for (AddDeviceInfo info : addDeviceInfos) {
				if (!device.matches(info.pattern))
					continue;

				HashMap<String, Object> map;
				synchronized (idmap) {
					map = new HashMap<String, Object>(idmap);
				}

				if (info.add) {
					if (event.getEventType() != InfrastructureEventType.ANSWER_DEVICE
							&& event.getEventType() != InfrastructureEventType.NEW_DEVICE)
						continue;

					// prevent adding the same device mutliple times - only
					// react to the first ANSWER event of this device
					if (event.getEventType() == InfrastructureEventType.ANSWER_DEVICE) {
						if (info.answerDevices.contains(id))
							continue;
					}

					Object proxy = null;

					// check if interface matches
					DeviceInformationProvider ip;

					Socket sock = null;

					if (device.getType() == PublishType.SOCKET)
						try {
							ip = new NetworkPublisherInformationProvider(
									sock = new Socket(host, port));
						} catch (IOException e) {
							LOGGER.error(
									"Can't check interfaces of NetworkProxy for device "
											+ id, e);
							continue;
						}
					else if (device.getType() == PublishType.TUPLESPACE)
						try {
							ip = new TuplespacePublisherInformationProvider(
									host, port, true);
						} catch (TuplespaceException e) {
							LOGGER.error(
									"Can't check interfaces of TuplespaceProxy for device "
											+ id, e);
							continue;
						}
					else {
						LOGGER.error("Unknown PublishType: " + device.getType()
								+ " of device " + id);
						continue;
					}

					Type deviceType = ip.getDeviceType(id);
					Type[] types = ip.getInterfaces(deviceType);

					boolean hasType = false;
					for (Type t : types)
						// @formatter:off
						// test the string of the classes (if getJavaClass() returns null)
						// nonetheless, check if the class can be found and test
						// if the given class is a superclass of the device class
						// @formatter:on
						if (info.clazz.getName().equals(t.getJavaClassName())
								|| t.getJavaClass() != null
								&& info.clazz
										.isAssignableFrom(t.getJavaClass())) {

							hasType = true;
							break;
						}

					if (sock != null && sock.isConnected())
						try {
							sock.close();
						} catch (IOException e) {
							LOGGER.warn("error closing NetworkPublisherInformationProvider stream of device "
									+ id);
						}

					if (!hasType)
						continue;

					// create Proxy

					if (device.getType() == PublishType.SOCKET)
						try {
							proxy = NetworkProxy.createProxy(info.clazz, host,
									port);
						} catch (UnknownHostException e) {
							LOGGER.error(
									"Can't create NetworkProxy for device "
											+ id, e);
						} catch (IOException e) {
							LOGGER.error(
									"Can't create NetworkProxy for device "
											+ id, e);
						} catch (IllegalArgumentException e) {
							LOGGER.error(
									"Interface of the proxy does not match for device "
											+ id, e);
						}
					else if (device.getType() == PublishType.TUPLESPACE)
						try {
							proxy = TuplespaceProxy.createProxy(info.clazz,
									info.id, host, port);
						} catch (UnknownHostException e) {
							LOGGER.error(
									"Can't create TuplespaceProxy for device "
											+ id, e);
						} catch (IOException e) {
							LOGGER.error(
									"Can't create TuplespaceProxy for device "
											+ id, e);
						} catch (TuplespaceException e) {
							LOGGER.error(
									"Can't create TuplespaceProxy for device "
											+ id, e);
						} catch (TimeoutException e) {
							LOGGER.error(
									"Can't create TuplespaceProxy for device "
											+ id, e);
						} catch (IllegalArgumentException e) {
							LOGGER.error(
									"Interface of the proxy does not match for device "
											+ id, e);
						}
					else
						LOGGER.error("Unknown PublishType: " + device.getType()
								+ " of device " + id);

					if (proxy == null)
						continue;

					map.put("@PROXY", proxy);

				} else {
					// remove device
					if (event.getEventType() != InfrastructureEventType.DEVICE_REMOVED)
						continue;
				}

				synchronized (idmap) {
					map.putAll(idmap);
				}
				map.put("@ID", id);

				LOGGER.trace("Invoking method call " + info.methodCall
						+ " for device id " + id);

				ObjectDescriptor descr = new ObjectDescriptor(info.object,
						false);
				try {
					SerializerHelper.invokeObjectMethod(descr, info.methodCall,
							info.object.getClass().getClassLoader(), map);

					// prevent adding the same device mutliple times - only
					// react to the first ANSWER event of this device
					if (info.add
							&& event.getEventType() == InfrastructureEventType.ANSWER_DEVICE)
						info.answerDevices.add(id);

				} catch (MethodInvocationException e) {
					LOGGER.error("Could not invoke method " + info.methodCall
							+ " on object", e);
				} catch (NoMatchingMethodFoundException e) {
					LOGGER.error("Could not invoke method " + info.methodCall
							+ " on object", e);
				}
			}
		}
	}

	/**
	 * notify the object of new or removed devices according to
	 * {@link AddDeviceInfo}.
	 * 
	 * @param info
	 * 
	 * @throws Exception
	 */
	public void addDevice(AddDeviceInfo info) throws Exception {
		if (info.type != null && info.type != PublishType.SOCKET
				&& info.type != PublishType.TUPLESPACE)
			throw new Exception("Invalid PublishType: " + info.type);

		try {
			setupInfrastructure();
		} catch (InfrastructureException e) {
			throw new Exception(e);
		}

		synchronized (addDeviceInfos) {
			if (addDeviceInfos.isEmpty()) {
				// only add the observer the first time
				Observer<InfrastructureEvent> observer = new Observer<InfrastructureEvent>() {
					@Override
					public void notify(
							Observable<? extends InfrastructureEvent> sender,
							InfrastructureEvent event) {
						addDeviceNotify(event);
					}
				};

				infrastructure.addObserver(observer);
			}

			addDeviceInfos.add(info);
		}

		if (info.add) // only search for device if "add", not if "remove"
			try {
				infrastructure.searchDevice(info.pattern);
			} catch (InfrastructureException e) {
				throw new Exception(e);
			}

	}

	/**
	 * @return all {@link PublishHandle handles} of all published objects.
	 */
	public LinkedList<PublishHandle> getHandles() {
		return handles;
	}

	/**
	 * Set the {@link Tuplespace} to be used for writing additional information
	 * to. Use {@link #setDefaultTuplespace()} to use the default
	 * {@link Tuplespace}.
	 * 
	 * @throws Exception
	 * 
	 * @see #writeInformation(String, String, Object)
	 * @see #setDefaultTuplespace()
	 */
	public void setTuplespace(InetAddress host, int port) throws Exception {
		try {
			space = NetworkProxy.createProxy(Tuplespace.class,
					host.getHostAddress(), port);
		} catch (UnknownHostException e) {
			throw new Exception("Could not connect to Tuplespace", e);
		} catch (IOException e) {
			throw new Exception("Could not connect to Tuplespace", e);
		}

		tuplespaceHost = host.getHostAddress();
		tuplespacePort = port;
	}

	private final static Object setDefaultTuplespaceMonitor = new Object();

	/**
	 * Connects to the default {@link Tuplespace} (as found in the
	 * {@link DistributedInfrastructure}) to be used for writing additional
	 * information to.
	 * 
	 * @see #setTuplespace(InetAddress, int)
	 */
	public void setDefaultTuplespace() throws Exception {
		synchronized (setDefaultTuplespaceMonitor) {
			if (space != null)
				return;

			System.out.println("Searching for a Tuplespace to publish");

			PublishedDeviceDescription descr = discoverSpace();
			setTuplespace(descr.getHost(), descr.getPort());
		}
	}

	/**
	 * Closes the publisher and removes the device from the
	 * {@link Infrastructure}.
	 * 
	 * @param handle
	 *            The {@link PublishHandle handle} returned by the
	 *            publish*()-Methods
	 * @throws InfrastructureException
	 *             when removing the device from the infrstructure failed
	 */
	public void unpublish(PublishHandle handle) throws InfrastructureException {
		if (handle.isClosed())
			return;

		if (handle instanceof TuplespacePublishHandle
				|| handle instanceof POJOPPublishHandle) {
			if (handle instanceof TuplespacePublishHandle)
				((TuplespacePublishHandle) handle).getPublisher().close();
			else if (handle instanceof POJOPPublishHandle)
				((POJOPPublishHandle) handle).getServer().stopListening();

			if (infrastructure != null && handle.getDeviceDescription() != null)
				infrastructure.removeDevice(handle.getDeviceDescription());
		} else if (handle instanceof PubSubPublishHandle) {
			PubSubPublishHandle h = (PubSubPublishHandle) handle;
			try {
				PublishSubscribeImpl.getInstance().removeObservable(
						h.getObservable());
			} catch (PublishSubscribeException e) {
				LOGGER.warn("Could not remove Obserable", e);
			}
		}

		handle.setClosed();
	}

	/**
	 * calls {@link #unpublish(PublishHandle)} for all {@link PublishHandle
	 * handles} in {@link #getHandles()}
	 * 
	 * @throws InfrastructureException
	 *             when removing a device from the infrstructure failed - no
	 *             more devices are then removed
	 */
	public void unpublishAll() throws InfrastructureException {
		for (PublishHandle handle : handles)
			unpublish(handle);
	}

	/**
	 * calls {@link #unpublishAll()}. If the device implements {@link Quittable}
	 * , calls {@link Quittable#quit()}.
	 */
	public void shutDown() {
		try {
			unpublishAll();
		} catch (InfrastructureException e) {
			LOGGER.error("Could not unpublish all", e);
		}

		try {
			for (Integer id : pubSubSubscribeIDs)
				PublishSubscribeImpl.getInstance().unsubscribe(id);
		} catch (PublishSubscribeException e) {
			LOGGER.error("Could not unsubscribe", e);
		}
	}

	public ConsoleWrapper initConsoleWrapper(String title) {
		consoleWrapper = ConsoleWrapper.createInstance(false, null);
		consoleWrapper.setTitle(title);

		consoleWrapper.addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				shutDown();
				System.exit(0);
			}
		});

		return consoleWrapper;
	}
}
